/*
 * Open LiveReplay Server - Live replay server for Supreme Commander
 * 
 * Copyright (C) 2009, 2009 Open LiveReplay Server Project
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details:
 * http://www.gnu.org/copyleft/lesser.html#TOC3
 */
package net.galacticwar.replayserver;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.galacticwar.lua.LittleEndianInputStream;

import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoSession;

/**
 * 
 * A replay is used to store data coming from a sender or a file
 * 
 * Listeners can subscribe to this class in order to get updated each time new data arrives
 * 
 * @author Yoann Meste (aka Mancer)
 *
 */
public class LiveReplay{
	private static final Logger logger = Logger.getLogger("ReplayServer");

	private static int SAVE_DELAY = 1*60*1000; //Replay are saved to disk 1 minute after the start
	private static int SAVE_PERIOD = 30*1000; //Replay are saved to disk every 30 seconds
	private static int INITIAL_SIZE = 524288; 
	private static int MAX_REPLAY_SIZE = 8388608; // replays can't be upper than 8Mo
	
	// the replay's name
	private String name;

	private byte[] data;
	private int index = 0;
	
	// The replay starting date
	private Date date;
	
	// The last activity counter
	private long lastActivity;
	
	//The replay file
	private File file;
	
	// This index keeps the value of byte saved in the file
	private int saveIndex = 0;

	// the IoSession of the sender
	private IoSession sender;

	// the listener list
	private Set<IoSession> listeners = new CopyOnWriteArraySet<IoSession>();
	
	private Replay replay = null;

	private SaveTask saveTask = new SaveTask();

	/**
	 * Create a new {@code Replay}
	 * 
	 * @param name
	 *            A {@code String} corresponding to the name of this replay
	 * @param session
	 *            the {@code IoSession} of the sender
	 */
	public LiveReplay(String name, IoSession session) {
		if (session==null) {
			throw new NullPointerException("Session must not be null");
		}

		if (name==null) {
			throw new NullPointerException("Name must not be null");
		}
		
		if (name.equals("")) {
			throw new InvalidParameterException("Name must not be empty");
		}


		this.name = name;
		this.sender = session;
		data = new byte[INITIAL_SIZE];

		session.setAttribute("REPLAY", this);
		
		this.date = new Date();
		String d = date.toString();
		d = d.replaceAll(":", "_");

		String filename = d + " " + name;

		file = new File(filename);
		
		Timer timer = new Timer();
		timer.scheduleAtFixedRate(saveTask, SAVE_DELAY, SAVE_PERIOD);
		
		lastActivity = System.currentTimeMillis();
	}

	/**
	 * Create a new Replay from a file
	 * 
	 * @param file
	 * @throws IOException
	 */
	public LiveReplay(File file) throws IOException {
		if (file==null) {
			throw new NullPointerException("File must not be null");
		}

		this.date = new Date();
		this.file = file;
		load(file);
		lastActivity = System.currentTimeMillis();
	}

	/**
	 * Append new data received from the sender to this replay
	 * 
	 * @param newData
	 *            {@code Array} the new replay data
	 */
	public synchronized void addData(byte[] newData) {
		if (newData==null) {
			return;
		}

		// Calculate the new replay size
		int newcount = index + newData.length;
		
		if (newcount > MAX_REPLAY_SIZE) {
			logger.log(Level.WARNING, "Max replay size limit reached");
			return;
		}
		
		if (newcount > data.length) {
			data = Arrays.copyOf(data, Math.max(data.length << 1, newcount));
		}

		// Adding data in the array
		System.arraycopy(newData, 0, data, index, newData.length);
		index = newcount;

		// broadcast to everyone
		broadcast();
		
		// Try to parse the replay content
		parseReplay();
		
		lastActivity = System.currentTimeMillis();
	}

	/**
	 * Add a new listener to this replay.
	 * 
	 * @param session
	 *            the {@code IoSession} of the new listener.
	 */
	public void addListener(IoSession session) {
		if (session==null) {
			return;
		}

		ByteBuffer buffer = ByteBuffer.wrap(data, 0, index);
		session.setAttribute("OFFSET", new Offset(index));
		session.write(buffer);

		session.setAttribute("REPLAY", this);

		listeners.add(session);
		
		lastActivity = System.currentTimeMillis();
	}

	/**
	 * 
	 * @param session
	 *            the {@code IoSession} to remove from listener list.
	 */
	public void removeListener(IoSession session) {
		if (session==null) {
			return;
		}
		session.setAttribute("REPLAY", null);

		if (session == sender) {

			// Stopping the save task
			saveTask.cancel();
			saveTask = null;
			
			logger.log(Level.INFO, "The sender of replay {0} has closed", name);

			// Saving the replay to the file
			try {
				appendToFile();
			} catch (IOException e) {
				logger.log(Level.WARNING, "Error during saving replay", e);
			}
			
			sender = null;

		}
		listeners.remove(session);
	}

	/**
	 * Retrieve the sender of this replay
	 * 
	 * @return the {@code IoSession} of the sender of this replay
	 */
	public IoSession getSender() {
		return sender;
	}
	
	/**
	 * Returns the name of this Replay
	 * 
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * Returns the list of all listeners of this replay
	 * 
	 */
	public Set<IoSession> getListeners() {
		return listeners;
	}
	
	/**
	 * Returns the stating date of this replay
	 * 
	 */
	public Date getStartingDate() {
		return date;
	}
	
	/**
	 * Return the last activity time
	 * 
	 */
	public long getLastActivity() {
		return lastActivity;
	}
	
	/**
	 * return the {@code Replay} informations if there is enough data
	 * @return
	 */
	public Replay getReplay() {
		if (replay==null) {
			replay = parseReplay();
		}
		return replay;
	}
	
	/**
	 * Returns the status of this replay
	 *
	 */
	public boolean isInProgress(){
		if (sender == null || sender.isConnected() ==false ) {
			return false;
		}
		return true;
	}
	
	/**
	 * This function closes all sessions attached to this replay
	 */
	public void close(){
		logger.log(Level.INFO, "Closing replay {0}", name);
		
		// closing all actives sessions
		if (sender != null && sender.isConnected()) {
			sender.close();
			removeListener(sender);
		}
		for (IoSession session : listeners) {
			if (session.isConnected()) {
				removeListener(session);
			}
		}
		
		if (saveTask!=null) {
			//  stopping the saving task if active
			saveTask.cancel();
			// saves all remaining data to file
			saveTask.run();
			
			saveTask = null;
		}
		
	}
	
	/**
	 * Parse the replay data if there is enough data available
	 * <p>
	 * If the data format is invalid, this method close this current Live Replay
	 * @return the {@code Replay} corresponding to this LiveReplay
	 */
	private Replay parseReplay() {
		// if we got enough byte, we try to parse the replay header
		if (replay == null && index > 4096) {
			logger.log(Level.INFO, "Parsing replay {0}", name);
			
			replay = new Replay();
			ByteArrayInputStream bis = new ByteArrayInputStream(data);
			LittleEndianInputStream in = new LittleEndianInputStream(bis);
			try {
				replay.read(in);
				
				boolean allowed = ReplayFilter.getInstance().isReplayAllowed(replay);
				if (! allowed) {
					logger.log(Level.WARNING, "The sender of this is not the allowed list -> Closing replay", name);
					this.close();
					replay = null;
				}
			}
			catch (UnknownReplayFormatException e) {
				//This is not a valid replay, so we close it
				this.close();
				replay = null;
			}
			catch (IOException e) {
				// we do not get enough data to parse the replay
				replay =null;
			}
		}
		return replay;
	}

	/**
	 * Send new replay data to all listeners. This loop is very time and memory
	 * critical
	 */
	private void broadcast() {
		for (IoSession session : listeners) {
			if (session.isConnected()) {
				Offset offset = (Offset) session.getAttribute("OFFSET");

				ByteBuffer buffer = ByteBuffer.wrap(data, offset.value, index
						- offset.value);

				offset.value = index;

				session.write(buffer);
			}
		}
	}
	
	
	

	/**
	 * Append new replay data to a file
	 * 
	 * @param file
	 *            the {@code File} where the data should be added
	 * @throws IOException
	 */
	private synchronized void appendToFile() throws IOException {
		final boolean APPEND = true;
		
		FileOutputStream fos = new FileOutputStream(file, APPEND);
		fos.write(data, saveIndex, index - saveIndex);
		fos.flush();
		fos.close();
		logger.log(Level.FINE, "Replay data appended to {0}", file.getName());
		saveIndex = index;
	}
	
	
	/**
	 * Save this replay to a file
	 * 
	 * @param file
	 *            the {@code File} to be saved
	 * @throws IOException
	 */
	public synchronized void save(File file) throws IOException {
		FileOutputStream fos = new FileOutputStream(file);
		fos.write(data, 0, index);
		fos.flush();
		fos.close();
		logger.log(Level.INFO, "Replay {0} written to disk", file.getName());
	}


	/**
	 * 
	 * Load a replay from a file
	 * 
	 * @param file
	 *            The {@code File} to be read
	 * @throws IOException
	 */
	private synchronized void load(File file) throws IOException {
		FileInputStream fis = new FileInputStream(file);
		data = new byte[fis.available()];
		fis.read(data);

		index = data.length;
		name = file.getName();
		sender = null;
	}

	
	private class SaveTask extends TimerTask{
		@Override
		public void run() {
			// save task
			try {
				appendToFile();
			} catch (IOException e) {
				//cancelling the save task in case of error 
				this.cancel();
				logger.log(Level.WARNING, "Error during saving replay", e);
			}
		}
	}

	
	
	/**
	 * 
	 * This class is used as a mutable Integer. It is more GC friendly during
	 * the broadcast
	 * 
	 * @author Mancer
	 * 
	 */
	private class Offset {
		int value;

		public Offset(int value) {
			this.value = value;
		}
	}

}
